package ex3.taskqueue;

/**
 * @author Omer
 *
 */

import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.Naming;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import ex3.executor.Executor;
import ex3.matchmaker.Matchmaker;

@SuppressWarnings("serial")
public class TaskQueueImpl extends UnicastRemoteObject implements TaskQueue {
	
	//	TaskQueue entry point
	public static void main(String[] args) {
		
		//	Instantiating a new TaskQueue
		try{
			taskQueue = new TaskQueueImpl();
			
			taskQueue.matchmakerIP = args[0];
			taskQueue.matchmakerRegPort = args[1];
			taskQueue.inputFilename = args[2];
			
			//	Connecting to the RMI registry
			taskQueue.SetupRMI();
			
			//	Invoking async reading from the input file
			taskQueue.ReadFromFile();
		}
		catch (RemoteException re){
			System.out.println("Error instantiating task queue: " + re.getMessage());
		}
	}
	
	//	Notifing the op's OpQueue to wake up if needed
	public void WakeupOp(char op) throws RemoteException {
		opQueueLock.readLock().lock();
			OpQueue oq = opQueuesMap.get(op);
		opQueueLock.readLock().unlock();
		
		if (oq == null){
			//	If we got here - it's an error - the oq that sleeps isn't in the map
			return;
		}
		
		//	Taking OpQueue's lock, and waking him if he's waiting on it
		synchronized (oq) {
			oq.notify();	
		}
	}
	
	//	Run a task on a new thread
	public void RunOp(Executor e, char op, int opID, int op1, int op2, OpQueue caller){
		ExecuteTask runner = new ExecuteTask(e, op, opID, op1, op2, caller);
		threadPool.execute(runner);
	}
	
	//	Add a task to an OpQueue
	public void AddTask(char op, int opID, int op1, int op2, boolean addToFront) {
		
		//	Taking the OpQueueMap Lock
		opQueueLock.readLock().lock();
			OpQueue oq = opQueuesMap.get(op);
		opQueueLock.readLock().unlock();
		
		if (oq == null){	//	OpQueue for op hasn't instantiated yet
			if (addToFront){
				//	Error: queue was destroyed
				System.out.println("Error: queue was destroyed.");
				System.exit(1);
			}
			else{
				//	Creating the new OpQueue, adding it to the map and adding the task to it
				oq = new OpQueue(op);
				oq.AddTask(opID, op1, op2, addToFront);
				
				//	Taking the OpQueueMap Lock
				opQueueLock.writeLock().lock();
					opQueuesMap.put(op, oq);
				opQueueLock.writeLock().unlock();
				
				//	Running the OpQueue in a new thread
				AddThread();
				threadPool.execute(oq);	
			}	
		}
		else
		{
			//	Adding a task to the OpQueue
			oq.AddTask(opID, op1, op2, addToFront);
		}
	}
	
	//	Finished a task. Removing it from the opQueuesMap
	public void RemoveTaskQueue(Character op){
		opQueueLock.writeLock().lock();
			opQueuesMap.remove(op);
		opQueueLock.writeLock().unlock();
	}

	//	Counting the number of threads so that we can close the thread pool when needed
	public void AddThread(){
		synchronized (threadNum) {
			threadNum++;
		}
	}
	
	//	If all threads were destroyed we can shutdown the application
	public void DestroyThread(){
		synchronized (threadNum) {
			threadNum--;
		}
		
		if (threadNum == 0){
			threadPool.shutdown();
			//System.out.println(taskQueueRMIName + ": Finished all tasks.");
			System.exit(0);
		}
	}
	
	//	Checking if finished reading input file
	public boolean FinishedReadingInput(){
		finishedReadingLock.readLock().lock();
			boolean tmp = finishedReadingInput;
		finishedReadingLock.readLock().unlock();
		return tmp;
	}
	
	//	Set finished reading from file flag
	public void SetFinishedReading() {
		finishedReadingLock.writeLock().lock();
			finishedReadingInput = true;
		finishedReadingLock.writeLock().unlock();
	}
	
	//	Writing output method
	public void WriteToOutputFile(String output){
		synchronized (printLock) {
			try {
				writer.write(output);
				writer.newLine();
				writer.flush();
			} catch (IOException e) {
				System.out.println("Error writing output. Exiting.");
				System.exit(1);
				e.printStackTrace();
			}
		}
	}
	
	//	Private C'tor
	private TaskQueueImpl() throws RemoteException {
		threadPool = new ThreadPoolExecutor(10,150,30, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
		opQueuesMap = new HashMap<Character, OpQueue>();
		printLock = new Object();
		threadNum = new Integer(0);
		finishedReadingLock = new ReentrantReadWriteLock();
		opQueueLock = new ReentrantReadWriteLock();
	}
	
	//	Setting up all RMI stuff
	private void SetupRMI(){
		//	Getting Matchmaker from RMI
		try { 
            matchmakerRef = (Matchmaker)Naming.lookup("//" +  matchmakerIP + ":" + 
            											matchmakerRegPort + "/matchmaker");
            if (matchmakerRef == null){
				System.out.println("Error - matchmaker retrived is null.");
	            System.out.println("Exiting.");  
	            System.exit(1);
			}
		} catch (Exception e) { 
            System.out.println("TaskQueue failed to find matchmaker: " + e.getMessage());
            System.out.println("Exiting."); 
            e.printStackTrace(); 
            System.exit(1);
        } 
		
		//	Setting TQ to the RMI
		int myID = 0;
		try { 
            myID = matchmakerRef.assignTaskQueueID(); 
        } catch (Exception e) { 
            System.out.println("TaskQueue failed receive ID: " + e.getMessage());
            System.out.println("Exiting.");
            e.printStackTrace();
            System.exit(1);
        } 
        taskQueueRMIName = "TaskQueue" + myID;
		try{
			Naming.rebind("//" + matchmakerIP + ":" + matchmakerRegPort + "/" + taskQueueRMIName,this);
		}catch(Exception e){
			try {
				Naming.bind("//" + matchmakerIP + ":" + matchmakerRegPort + "/" + taskQueueRMIName,this);
			} catch (Exception e2) {
				System.out.println("Error binding " + taskQueueRMIName + " to RMI.");
				System.out.println("Exiting.");
				e2.printStackTrace();
				System.exit(1);
			}
		}
	}
	
	//	Opening file and reading from file in a new thread
	private void ReadFromFile(){
		try {
			reader = new FileReader(inputFilename);
			fileWriter = new FileWriter(inputFilename + ".out");
			writer = new BufferedWriter(fileWriter);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
			System.out.print("Error - File " + inputFilename + " not found.");
			System.exit(1);
		} catch (IOException e) {
			e.printStackTrace();
			System.out.print("Error - Can't create " + inputFilename + ".out.");
			System.exit(1);
		}
		
		//	Execute reading in a new thread.
		TaskFileReader tfr = new TaskFileReader(reader);
		AddThread();
		threadPool.execute(tfr);
	}
	
	//	Public members
	public String matchmakerIP;
	public String matchmakerRegPort;
	public static TaskQueueImpl taskQueue;
	public static Matchmaker matchmakerRef;
	public String taskQueueRMIName;
	public Object printLock;
	
	
	//	Private members
	private String inputFilename;
	private boolean finishedReadingInput;
	private ReentrantReadWriteLock finishedReadingLock;
	private ReentrantReadWriteLock opQueueLock;
	private HashMap<Character, OpQueue> opQueuesMap;
	private ThreadPoolExecutor threadPool;
	private FileReader reader;
	private FileWriter fileWriter;
	private BufferedWriter writer;
	private Integer threadNum;
}
